package top.byteio.common.s3;

import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.multipart.MultipartFile;
import software.amazon.awssdk.core.exception.SdkClientException;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.*;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest;
import top.byteio.common.core.constants.ContentType;
import top.byteio.common.core.constants.DateConstants;
import top.byteio.common.s3.config.S3Properties;
import top.byteio.common.s3.pojo.Bkt;
import top.byteio.common.s3.pojo.Storage;
import top.byteio.common.s3.strategy.StorageKeyStrategy;
import top.byteio.common.s3.utils.StorageUtils;

import java.io.InputStream;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.stream.Collectors;

/**
 * aws-s3协议通用存储操作.支持所有兼容s3协议的云存储:
 * {阿里云OSS，腾讯云COS，七牛云，京东云，minio 等}
 *
 * @author lerry
 * @since 2023/6/29 21:46
 */
@RequiredArgsConstructor
@Slf4j
public class S3Template {

    private final S3Properties s3Properties;

    private final S3Client s3Client;

    private final S3Presigner s3Presigner;

    private final StorageKeyStrategy storageKeyStrategy;

    /**
     * 创建bucket
     *
     * @param bucketName bucket名称
     * @return java.lang.String null则失败
     * @author lerry
     * @since 2023/6/29 22:22
     */
    public String createBucket(String bucketName) {
        if (!s3Client.headBucket(HeadBucketRequest
                .builder()
                .bucket(bucketName)
                .build())
                .accessPointAlias()) {

            CreateBucketResponse response = s3Client.createBucket(CreateBucketRequest
                    .builder()
                    .bucket(bucketName)
                    .build());
            String location = response.location();
            if (location != null && location.startsWith("/")) return location.substring(1);
            return location;
        }
        return null;
    }

    /**
     * 获取全部bucket
     *
     * @return java.util.List<com.amazonaws.services.s3.model.Bucket> 桶
     * @author lerry
     * @see <a href="http://docs.aws.amazon.com/goto/WebAPI/s3-2006-03-01/ListBuckets"> 详情查看WAS-3S API文档</a>
     * @since 2023/6/29 22:23
     */
    @SneakyThrows
    public List<Bkt> getAllBuckets() {
        return s3Client.listBuckets().buckets()
                .stream()
                .map(bucket -> Bkt.builder()
                        .bucketName(bucket.name())
                        .createdAt(bucket.creationDate()
                                .atZone(ZoneId.of(DateConstants.UTC_ZONEId))
                                .format(DateTimeFormatter.ofPattern(DateConstants.YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)))
                        .regionName(bucket.bucketRegion())
                        .build())
                .collect(Collectors.toList());
    }


    /**
     * 获取桶信息
     *
     * @param bucketName bucket名称
     * @return java.util.Optional<com.amazonaws.services.s3.model.Bucket>
     * @author lerry
     * @see <a href="http://docs.aws.amazon.com/goto/WebAPI/s3-2006-03-01/ListBuckets"> 详情查看WAS-3S API文档</a>
     * @since 2023/6/29 22:26
     */
    @SneakyThrows
    public Bkt getBucket(String bucketName) {
        return s3Client.listBuckets().buckets()
                .stream()
                .filter(bucket -> bucket.name().equals(bucketName))
                .map(bucket -> Bkt.builder()
                        .bucketName(bucket.name())
                        .createdAt(bucket.creationDate()
                                .atZone(ZoneId.of(DateConstants.UTC_ZONEId))
                                .format(DateTimeFormatter.ofPattern(DateConstants.YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)))
                        .regionName(bucket.bucketRegion())
                        .build())
                .findFirst()
                .orElse(null);
    }


    /**
     * 删除桶
     *
     * @param bucketName bucket名称
     * @author lerry
     * @see <a href="http://docs.aws.amazon.com/goto/WebAPI/s3-2006-03-01/DeleteBucket">详情查看WAS-3S API文档</a>
     * @since 2023/6/29 22:27
     */
    @SneakyThrows
    public boolean removeBucket(String bucketName) {
        DeleteBucketResponse response = s3Client.deleteBucket(DeleteBucketRequest
                .builder()
                .bucket(bucketName)
                .build());
        return response.sdkHttpResponse().isSuccessful();
    }

    /**
     * 根据文件前置查询文件
     *
     * @param bucketName bucket名称
     * @param prefix     前缀
     * @return S3Object 列表
     * @author lerry
     * @see <a href="http://docs.aws.amazon.com/goto/WebAPI/s3-2006-03-01/ListObjects">详情查看WAS-3S API文档</a>
     * @since 2023/6/29 22:27
     */
    public List<Storage> getAllObjectsByPrefix(String bucketName, String prefix) {
        List<Storage> result = new ArrayList<>();
        String continuationToken = null;

        do {
            ListObjectsV2Request.Builder requestBuilder = ListObjectsV2Request.builder()
                    .bucket(bucketName)
                    .prefix(prefix);

            if (continuationToken != null) {
                requestBuilder.continuationToken(continuationToken);
            }

            ListObjectsV2Response response = s3Client.listObjectsV2(requestBuilder.build());

            if (response.contents() != null) {
                result.addAll(response.contents().stream()
                        .map(object -> Storage.builder()
                                .key(object.key())
                                .eTag(object.eTag())
                                .bucket(bucketName)
                                .prefix(StorageUtils.getObjectPrefix(object.key()))
                                .objectName(storageKeyStrategy.restore(object.key()))
                                .fileSize(object.size())
                                .fileType(StorageUtils.getObjectType(object.key()))
                                .build())
                        .toList());
            }

            continuationToken = response.nextContinuationToken();
        } while (continuationToken != null);

        return result;
    }


    /**
     * 获取文件外链
     * 具体要求见
     * {@link software.amazon.awssdk.services.s3.presigner.S3Presigner#presignGetObject}
     * @param bucketName bucket名称
     * @param key 文件名称
     * @param expiration 过期时间（分钟）
     * @return url 文件外链
     */
    public String getObjectURL(String bucketName, String key, Long expiration) {
        // 参数校验
        if (bucketName == null || bucketName.isEmpty()) {
            throw new IllegalArgumentException("bucketName cannot be null or empty");
        }
        if (key == null || key.isEmpty()) {
            throw new IllegalArgumentException("objectName cannot be null or empty");
        }
        if (expiration == null || expiration <= 0) {
            throw new IllegalArgumentException("expiration must be positive");
        }

        try {
            GetObjectRequest getObjectRequest = GetObjectRequest.builder()
                    .bucket(bucketName)
                    .key(key)
                    .build();

            Instant now = Instant.now();
            Instant expirationTime = now.plusSeconds(expiration * 60);

            PresignedGetObjectRequest presignedRequest = s3Presigner.presignGetObject(
                    GetObjectPresignRequest
                            .builder()
                            .signatureDuration(Duration.between(now, expirationTime))
                            .getObjectRequest(getObjectRequest)
                            .build());
            return presignedRequest.url().toString();
        } catch (Exception e) {
            log.error("获取文件外链失败: bucketName={}, objectName={}, expiration={}",
                      bucketName, key, expiration, e);
            throw new RuntimeException("Failed to generate presigned URL", e);
        }
    }



    /**
     * 获取文件
     *
     * @param bucketName bucket名称
     * @param key 文件名称
     * @return 二进制流
     * @author lerry
     * @see <a href="http://docs.aws.amazon.com/goto/WebAPI/s3-2006-03-01/GetObject">AWS
     * API Documentation</a>
     * @since 2023/6/29 22:27
     */
    @SneakyThrows
    public ByteBuffer getObject(String bucketName, String key) {
        return s3Client.getObjectAsBytes(
                GetObjectRequest
                        .builder()
                        .bucket(bucketName)
                        .key(key)
                        .build())
                .asByteBuffer();
    }

    /**
 * 分片上传文件
 *
 * @param bucketName bucket名称
 * @param objectName 文件名称
 * @param file       要上传的文件
 * @param prefix     文件前缀
 * @param partSize   分片大小(字节)，建议不小于5MB
 * @return ETag 文件标识
 * @author lerry
 * @since 2023/6/29 22:27
 */
public String multipartUpload(String bucketName, String objectName, MultipartFile file, String prefix, long partSize) {
    String uploadId = null;
    String key = null;
    try {
        // 参数校验
        if (bucketName == null || bucketName.trim().isEmpty()) {
            throw new IllegalArgumentException("bucketName cannot be null or empty");
        }
        if (objectName == null || objectName.trim().isEmpty()) {
            throw new IllegalArgumentException("objectName cannot be null or empty");
        }
        if (file == null) {
            throw new IllegalArgumentException("file cannot be null");
        }
        if (file.isEmpty()) {
            throw new IllegalArgumentException("file cannot be empty");
        }
        if (partSize < 5 * 1024 * 1024) { // 最小5MB
            log.warn("Part size is less than 5MB, this may reduce upload performance");
        }
        if (partSize > Integer.MAX_VALUE) {
            throw new IllegalArgumentException("partSize exceeds maximum allowed size");
        }

        // 生成唯一文件名
        key = storageKeyStrategy.generate(objectName);

        if (prefix != null && !prefix.isEmpty()) {
            key = prefix.endsWith("/") ? prefix + key : prefix + "/" + key;
        }

        // 初始化分片上传
        CreateMultipartUploadRequest createRequest = CreateMultipartUploadRequest.builder()
                .bucket(bucketName)
                .key(key)
                .contentType(ContentType.APPLICATION_OCTET_STREAM)
                .build();

        CreateMultipartUploadResponse createResponse = s3Client.createMultipartUpload(createRequest);
        uploadId = createResponse.uploadId();

        // 收集已上传分片的信息
        List<CompletedPart> completedParts = Collections.synchronizedList(new ArrayList<>());

        // 读取文件数据并分片上传
        try (InputStream inputStream = file.getInputStream()) {
            byte[] buffer = new byte[(int) partSize];
            int partNumber = 1;
            int bytesRead;

            List<Future<CompletedPart>> futures;
            try (ExecutorService executor = Executors.newFixedThreadPool(5)) {
                futures = new ArrayList<>();

                while ((bytesRead = inputStream.read(buffer)) != -1) {
                    final int currentPartNumber = partNumber;
                    final byte[] partData = bytesRead == buffer.length ? buffer : Arrays.copyOf(buffer, bytesRead);

                    String finalUploadId = uploadId;
                    String finalKey = key;

                    Future<CompletedPart> future = executor.submit(() -> {
                        UploadPartRequest uploadRequest = UploadPartRequest.builder()
                                .bucket(bucketName)
                                .key(finalKey)
                                .uploadId(finalUploadId)
                                .partNumber(currentPartNumber)
                                .checksumSHA256("UNSIGNED-PAYLOAD")
                                .contentLength((long) partData.length)
                                .build();

                        UploadPartResponse uploadResponse = s3Client
                                .uploadPart(uploadRequest, RequestBody.fromBytes(partData));

                        return CompletedPart.builder()
                                .partNumber(currentPartNumber)
                                .eTag(uploadResponse.eTag())
                                .build();
                    });

                    futures.add(future);
                    partNumber++;
                }

                executor.shutdown();
            } // 并发上传线程数
            for (Future<CompletedPart> future : futures) {
                completedParts.add(future.get());
            }
        }

        // 完成分片上传
        completedParts.sort(Comparator.comparingInt(CompletedPart::partNumber));

        CompletedMultipartUpload completedMultipartUpload = CompletedMultipartUpload.builder()
                .parts(completedParts)
                .build();

        CompleteMultipartUploadRequest completeRequest = CompleteMultipartUploadRequest.builder()
                .bucket(bucketName)
                .key(key)
                .uploadId(uploadId)
                .checksumSHA256("UNSIGNED-PAYLOAD")
                .multipartUpload(completedMultipartUpload)
                .build();

        CompleteMultipartUploadResponse completeResponse = s3Client.completeMultipartUpload(completeRequest);
        return completeResponse.eTag();

    } catch (Exception e) {
        // 如果上传过程中出现异常，取消分片上传
        if (uploadId != null && key != null) {
            try {
                AbortMultipartUploadRequest abortRequest = AbortMultipartUploadRequest.builder()
                        .bucket(bucketName)
                        .key(key)
                        .uploadId(uploadId)
                        .build();
                s3Client.abortMultipartUpload(abortRequest);
            } catch (Exception abortException) {
                log.error("取消分片上传失败: bucketName={}, objectName={}", bucketName, objectName, abortException);
            }
        }
        log.error("分片上传失败: bucketName={}, objectName={}", bucketName, objectName, e);
        throw new RuntimeException("分片上传失败", e);
    }
}


    /**
     * 获取文件信息
     *
     * @param bucketName bucket名称
     * @param key 文件名称
     * @author lerry
     * @see <a href="http://docs.aws.amazon.com/goto/WebAPI/s3-2006-03-01/GetObject">AWS
     * API Documentation</a>
     * @since 2023/6/29 22:27
     */
    public Storage getObjectInfo(String bucketName, String key) {
        try {
            HeadObjectRequest request = HeadObjectRequest.builder()
                    .bucket(bucketName)
                    .key(key)
                    .build();

            HeadObjectResponse response = s3Client.headObject(request);

            return Storage.builder()
                    .key(key)
                    .eTag(response.eTag())
                    .fileSize(response.contentLength())
                    .fileType(ContentType.OFFICE_WORD)
                    .objectName(storageKeyStrategy.restore(key))
                    .prefix(StorageUtils.getObjectPrefix(key))
                    .bucket(bucketName)
                    .build();
        } catch (Exception e) {
            log.error("获取文件信息失败: {}", e.getMessage(), e);
            return null;
        }
    }

    /**
     * 删除文件
     *
     * @param bucketName bucket名称
     * @param key 文件名称
     * @return boolean true: 成功 false：失败
     * @author lerry
     * @see <a href=
     * "http://docs.aws.amazon.com/goto/WebAPI/s3-2006-03-01/DeleteObject">AWS API
     * Documentation</a>
     * @since 2023/6/29 22:27
     */
    public boolean removeObject(String bucketName, String key) {
        try {
            s3Client.deleteObject(
                    DeleteObjectRequest
                            .builder()
                            .bucket(bucketName)
                            .key(key)
                            .build());
            return true;
        } catch (SdkClientException e) {
            log.error(e.toString());
            return false;
        }
    }
}

